const express = require('express');
const TurndownService = require('turndown');
const { HashManager } = require('civkit');
const md5Hasher = new HashManager('md5', 'hex');

const app = express();
const port = 3000;

// 解析 JSON 格式的请求体
app.use(express.json({ limit: '50mb' }));

function cleanAttribute(attribute) {
    return attribute ? attribute.replace(/(\n+\s*)+/g, '\n') : '';
}

function getTurnDownService(options) {

    // 创建 Turndown 实例
    const turnDownService = new TurndownService({
        codeBlockStyle: 'fenced',
        preformattedCode: true,
    });

    turnDownService.addRule('remove-irrelevant', {
        filter: ['meta', 'style', 'script', 'noscript', 'link', 'textarea', 'select'],
        replacement: () => ''
    });

    turnDownService.addRule('truncate-svg', {
        filter: 'svg',
        replacement: () => ''
    });
    turnDownService.addRule('title-as-h1', {
        filter: ['title'],
        replacement: (innerText) => `${innerText}\n===============\n`
    });

    if (options?.imgDataUrlToObjectUrl) {
        console.log('Adding data-url-to-pseudo-object-url rule');
        turnDownService.addRule('data-url-to-pseudo-object-url', {
            filter: (node) => Boolean(node.tagName === 'IMG' && node.getAttribute('src')?.startsWith('data:')),
            replacement: (_content, node) => {
                const src = (node.getAttribute('src') || '').trim();
                const alt = cleanAttribute(node.getAttribute('alt')) || '';

                if (options.url) {
                    const refUrl = new URL(options.url);
                    const mappedUrl = new URL(`blob:${refUrl.origin}/${md5Hasher.hash(src)}`);

                    return `![${alt}](${mappedUrl})`;
                }

                return `![${alt}](blob:${md5Hasher.hash(src)})`;
            }
        });
    }

    turnDownService.addRule('improved-paragraph', {
        filter: 'p',
        replacement: (innerText) => {
            const trimmed = innerText.trim();
            if (!trimmed) {
                return '';
            }

            return `${trimmed.replace(/\n{3,}/g, '\n\n')}\n\n`;
        }
    });

    turnDownService.addRule('improved-inline-link', {
        filter: function (node, options) {
            return Boolean(
                options.linkStyle === 'inlined' &&
                node.nodeName === 'A' &&
                node.getAttribute('href')
            );
        },

        replacement: function (content, node) {
            let href = node.getAttribute('href');
            if (href) href = href.replace(/([()])/g, '\\$1');
            let title = cleanAttribute(node.getAttribute('title'));
            if (title) title = ' "' + title.replace(/"/g, '\\"') + '"';

            const fixedContent = content.replace(/\s+/g, ' ').trim();
            let fixedHref = href.replace(/\s+/g, '').trim();
            if (options?.url) {
                try {
                    fixedHref = new URL(fixedHref, options.url).toString();
                } catch (_err) {
                    void 0;
                }
            }

            return `[${fixedContent}](${fixedHref}${title || ''})`;
        }
    });

    turnDownService.addRule('improved-code', {
        filter: function (node) {
            let hasSiblings = node.previousSibling || node.nextSibling;
            let isCodeBlock = node.parentNode.nodeName === 'PRE' && !hasSiblings;

            return node.nodeName === 'CODE' && !isCodeBlock;
        },

        replacement: function (inputContent) {
            if (!inputContent) return '';
            let content = inputContent;

            let delimiter = '`';
            let matches = content.match(/`+/gm) || [];
            while (matches.indexOf(delimiter) !== -1) delimiter = delimiter + '`';
            if (content.includes('\n')) {
                delimiter = '```';
            }

            let extraSpace = delimiter === '```' ? '\n' : /^`|^ .*?[^ ].* $|`$/.test(content) ? ' ' : '';

            return delimiter + extraSpace + content + (delimiter === '```' && !content.endsWith(extraSpace) ? extraSpace : '') + delimiter;
        }
    });

    let imgIdx = 0;
    turnDownService.addRule('img-generated-alt', {
        filter: 'img',
        replacement: (_content, node) => {
            let linkPreferredSrc = (node.getAttribute('src') || '').trim();
            if (!linkPreferredSrc || linkPreferredSrc.startsWith('data:')) {
                const dataSrc = (node.getAttribute('data-src') || '').trim();
                if (dataSrc && !dataSrc.startsWith('data:')) {
                    linkPreferredSrc = dataSrc;
                }
            }

            let src;
            try {
                src = new URL(linkPreferredSrc, options?.url).toString();
            } catch (_err) {
                void 0;
            }
            const alt = cleanAttribute(node.getAttribute('alt'));
            if (!src) {
                return '';
            }
            const imageIdxTrack = {};
            const imageSummary = {};
            const urlToAltMap = {};
            const mapped = urlToAltMap[src];
            const imgSerial = ++imgIdx;
            const idxArr = imageIdxTrack[src] ? imageIdxTrack[src] : [];
            idxArr.push(imgSerial);
            imageIdxTrack[src] = idxArr;
            const refUrl = new URL(options.url);
            normalUrl = refUrl;
            if (mapped) {
                imageSummary[src] = mapped || alt;

                if (src?.startsWith('data:') && imgDataUrlToObjectUrl) {
                    const mappedUrl = new URL(`blob:${nominalUrl?.origin || ''}/${md5Hasher.hash(src)}`);
                    mappedUrl.protocol = 'blob:';

                    return `![Image ${imgIdx}: ${mapped || alt}](${mappedUrl})`;
                }

                return `![Image ${imgIdx}: ${mapped || alt}](${src})`;
            }

            imageSummary[src] = alt || '';
            if (src?.startsWith('data:') && imgDataUrlToObjectUrl) {
                const mappedUrl = new URL(`blob:${nominalUrl?.origin || ''}/${md5Hasher.hash(src)}`);
                mappedUrl.protocol = 'blob:';

                return alt ? `![Image ${imgIdx}: ${alt}](${mappedUrl})` : `![Image ${imgIdx}](${mappedUrl})`;
            }

            return alt ? `![Image ${imgIdx}: ${alt}](${src})` : `![Image ${imgIdx}](${src})`;
        }
    });

    return turnDownService;
}

// 定义 /api/html2markdown 接口
app.post('/api/html2markdown', (req, res) => {
    try {
        // 从请求体中获取 HTML 字符串
        const { html, options } = req.body;

        // 检查是否提供了 HTML 字符串
        if (!html) {
            return res.status(400).json({ error: 'Missing "html" field in the request body' });
        }

        // 将 HTML 转换为 Markdown
        const turnDownService = getTurnDownService(options || {});
        const markdown = turnDownService.turndown(html);

        // 返回 Markdown 字符串
        res.json({ markdown });
    } catch (error) {
        console.error('Error:', error);
        // 处理异常
        res.status(500).json({ error: 'An error occurred while converting HTML to Markdown' });
    }
});

// 启动服务器
app.listen(port, () => {
    console.log(`Server is running on port ${port}`);
});